package com.lottemall.search.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.lottemall.common.enums.ExceptionEnume;
import com.lottemall.common.exception.LtException;
import com.lottemall.common.utils.JsonUtils;
import com.lottemall.common.utils.NumberUtils;
import com.lottemall.common.vo.PageResult;
import com.lottemall.item.pojo.*;
import com.lottemall.search.client.BrandClient;
import com.lottemall.search.client.CategoryClient;
import com.lottemall.search.client.GoodsClient;
import com.lottemall.search.client.SpecificationClient;
import com.lottemall.search.pojo.Goods;
import com.lottemall.search.pojo.SearchRequest;
import com.lottemall.search.pojo.SearchResult;
import com.lottemall.search.repository.GoodsRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author bystander
 * @date 2018/9/22
 */
@Slf4j
@Service
public class SearchService {
    @Autowired
    private BrandClient brandClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private CategoryClient categoryClient;

    //用于简单查询索引库的Template
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    //用于一些复杂查询索引库的Repository
    @Autowired
    private GoodsRepository goodsRepository;

    /**
     * 将spu转换成需要存入索引库的Goods
     *
     * @param spu
     * @return
     * @throws IOException
     */
    public Goods buildGoods(Spu spu) {
        Long spuId = spu.getId();
        //查询商品分类名
        List<String> names = categoryClient.queryByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()))
                .stream()
                .map(Category::getName)
                .collect(Collectors.toList());

        //查询商品品牌
        Brand brand = brandClient.queryBrandById(spu.getBrandId());
        if (brand == null) {
            throw new LtException(ExceptionEnume.BRAND_NOT_FOUND);
        }
        //所有的搜索字段拼接到all中，all存入索引库，并进行分词处理，搜索时与all中的字段进行匹配查询
        String all = spu.getTitle() + StringUtils.join(names, " ") + brand.getName();

        //查询sku
        List<Sku> skuList = goodsClient.querySkuBySpuId(spuId);

        if (CollectionUtils.isEmpty(skuList)) {
            throw new LtException(ExceptionEnume.GOODS_NOT_FOUND);
        }

        //存储price的集合
        TreeSet<Double> priceSet = new TreeSet<>();

        //设置存储skus的json结构的集合，用map结果转化sku对象，转化为json之后与对象结构相似（或者重新定义一个对象，存储前台要展示的数据，并把sku对象转化成自己定义的对象）
        List<Map<String, Object>> skus = new ArrayList<>();
        //从sku中取出要进行展示的字段，并将sku转换成json格式
        for (Sku sku : skuList) {
            priceSet.add(Double.valueOf(sku.getPrice()));
            HashMap<String, Object> map = new HashMap<>();
            map.put("id", sku.getId());
            map.put("title", sku.getTitle());
            //sku中有多个图片，只展示第一张
            map.put("image", StringUtils.substringBefore(sku.getImages(), ","));
            map.put("price", sku.getPrice());
            skus.add(map);
        }


        //查询规格参数，规格参数中分为通用规格参数和特有规格参数
        List<SpecParam> params = specificationClient.queryParamByList(null, spu.getCid3(), true);
        if (CollectionUtils.isEmpty(params)) {
            throw new LtException(ExceptionEnume.SPEC_PARAM_NOT_FOND);
        }
        //查询商品详情
        SpuDetail spuDetail = goodsClient.querySpuDetailById(spuId);

        //获取通用规格参数
        Map<Long, String> genericSpec = JsonUtils.parseMap(spuDetail.getGenericSpec(), Long.class, String.class);
        //获取特有规格参数
        Map<Long, List<String>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
        });

        //定义spec对应的map
        HashMap<String, Object> map = new HashMap<>();
        //对规格进行遍历，并封装spec，其中spec的key是规格参数的名称，值是商品详情中的值
        for (SpecParam param : params) {
            //key是规格参数的名称
            String key = param.getName();
//            Object value = "";
            Object value = new Object();

            if (param.getGeneric()) {
                //参数是通用属性，通过规格参数的ID从商品详情存储的规格参数中查出值
                value = genericSpec.get(param.getId());
                if (param.getNumeric()) {
                    //参数是数值类型，处理成段，方便后期对数值类型进行范围过滤
                    if (value != null) {
                        value = chooseSegment(value.toString(), param);
                    }
                }
            } else {
                //参数不是通用类型
                value = specialSpec.get(param.getId());
            }
            value = (value == null ? "其他" : value);
            //存入map
            map.put(key, value);
        }
        Goods goods = new Goods();
        goods.setId(spuId);
        goods.setBrandId(spu.getBrandId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setCreateTime(spu.getCreateTime());
        goods.setAll(all);
        goods.setPrice(priceSet);
        goods.setSubTitle(spu.getSubTitle());
        goods.setSpecs(map);
        goods.setSkus(JsonUtils.serialize(skus));
        return goods;
    }

    /**
     * 将规格参数为数值型的参数划分为段
     *
     * @param value
     * @param p
     * @return
     */
    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }


    /**
     * 搜索功能
     * <p>
     * 类似这个没有加分页的原始es的json查询，这里同时过滤了，因为用户搜索我们只需要这三个字段
     * Get /goods/_search
     * {
     * "query": {
     * "match": {
     * "all": "手机"
     * }
     * },
     * "_source":["id","subTitle","skus"]
     * }
     * <p>
     * <p>
     * <p>
     *
     * @param searchRequest
     * @return
     */
    public PageResult<Goods> search(SearchRequest searchRequest) {
        //得到分页信息:当前页、每页大小
        int page = searchRequest.getPage() - 1; //记得减1，因为都是程序中一般都是默认认为0是第一页
        int size = searchRequest.getSize();
        //创建查询构建器
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        //结果过滤(第一个是允许返回的字段，第二个参数是不允许返回的)
        nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));
        //追加分页条件
        nativeSearchQueryBuilder.withPageable(PageRequest.of(page, size));
        //追加过滤条件：这个字段是Goods中的属性，同时也是存入索引库中的字段
        QueryBuilder basicQuery = buildBasicQuery(searchRequest);
        nativeSearchQueryBuilder.withQuery(basicQuery);

        //聚合分类和品牌的信息
        //聚合分类
        String categoryAggName = "category_agg";
        //聚合名字、聚合字段(3级目录精确了类)、
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        //聚合分类
        String brandAggName = "brand_agg";
        //聚合名字、聚合字段
        nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));


        //查询 (因为上面增添了聚合查询，打这个Repository只能返回分页结果，要向拿到聚合结果就要用elasticsearchTemplate，所以这里注释)
//        Page<Goods> goodsPage = goodsRepository.search(nativeSearchQueryBuilder.build());

        AggregatedPage<Goods> goodsPage = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), Goods.class);

        //解析分页结果：得到总页数、当前页数、查询结果
        long totalElements = goodsPage.getTotalElements();
        int totalPages = goodsPage.getTotalPages();
        List<Goods> goodsList = goodsPage.getContent();

        //解析聚合结果
        //首先拿到所有的聚合
        Aggregations aggregations = goodsPage.getAggregations();
        //分别解析分类和品牌的聚合结果,这里首先 通过聚合名称得到聚合结果，调用下面的方法
        List<Category> categoryList = parseCategoryAgg(aggregations.get(categoryAggName));
        List<Brand> brandList = parseBrandAgg(aggregations.get(brandAggName));

        //完成规格参数聚合
        List<Map<String, Object>> specs = null;
        //首先我们判断用户手上的结果只有一个分类的时候，我们再提供规格参数
        if (categoryList != null && categoryList.size() == 1) { //主语这个由短路效果，如果第一个不满足，不会去判断第二个，位置反了 可能会报错
            //商品分类只有一个，可以进行聚合规格参数，从而返回给前台
            specs = buildSpecificationAgg(categoryList.get(0).getId(), basicQuery);
        }
        //封装进我们制定好的分页Vo
        return new SearchResult(totalElements, totalPages, goodsList, categoryList, brandList, specs);
    }

    /**
     * 构建查询
     * inf
     * 此方法是后面改造搜索加入查询条件和过滤条件要分开
     *
     *   * 由于后面我们要加过滤条件所以
     *      * 查询写在must中，过滤条件写在filter中
     *      * Get /goods/_search
     *      * {
     *      * "query":{
     *      * "bool": {
     *      * "must": [
     *      * {}
     *      * ],
     *      * "filter": {}
     *      * }
     *      * }
     *      * }
     * @param searchRequest
     * @return
     */
    private QueryBuilder buildBasicQuery(SearchRequest searchRequest) {
        //创建布尔查询
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //查询条件
        boolQueryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()));
        //过滤条件
        //1.得到过滤项
        Map<String, String> filterMap = searchRequest.getFilter();
        if (CollectionUtils.isEmpty(filterMap)){
            return boolQueryBuilder;
        }
        //遍历添加过滤
        for (Map.Entry<String, String> stringStringEntry : filterMap.entrySet()) {
            String key = stringStringEntry.getKey();
            String value = stringStringEntry.getValue();
            //处理key
            if (!"cid3".equals(key) && !"brandId".equals(key)){
                key = "specs." + key + ".keyword"; //因为索引库中的过滤条件见识规格参数，我们需要这种形式
            }
            boolQueryBuilder.filter(QueryBuilders.termQuery(key,value));
        }
        return boolQueryBuilder;
    }

    /**
     * 聚合规格参数
     * <p>
     * <p>
     * request:
     * Get /goods/_search
     * {
     * "size": 0,
     * "aggs": {
     * "demo":{
     * "terms": {
     * "field": "specs.内存.keyword",
     * "size": 10
     * }
     * }
     * }
     * }
     * <p>
     * response:
     * "aggregations": {
     * "demo": {
     * "doc_count_error_upper_bound": 0,
     * "sum_other_doc_count": 0,
     * "buckets": [
     * {
     * "key": "4GB",
     * "doc_count": 76
     * },
     * {
     * "key": "3GB",
     * "doc_count": 49
     * },
     * {
     * "key": "6GB",
     * "doc_count": 49
     * },
     * {
     * "key": "2GB",
     * "doc_count": 23
     * },
     * {
     * "key": "8GB",
     * "doc_count": 2
     * },
     * {
     * "key": "5G",
     * "doc_count": 1
     * },
     * {
     * "key": "5GB",
     * "doc_count": 1
     * },
     * {
     * "key": "6G",
     * "doc_count": 1
     * }
     * ]
     * }
     *
     * @param id         商品 类型id
     * @param basicQuery 需要注意的是这个，需要在用户搜索的原有基础条件上进行聚合
     * @return
     */
    private List<Map<String, Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) {
        List<Map<String, Object>> specs = new ArrayList<>();
        //1. 查询需要聚合的规格参数
        List<SpecParam> specParams = specificationClient.queryParamByList(null, cid, true);
        //2. 聚合
        NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
        nativeSearchQueryBuilder.withQuery(basicQuery); //在原来查询条件的基础上去聚合， 因为一个手机分类中，如果用户输入的是小米key，所以该分类的苹果规格参数又不一样
        //遍历规格参数集合
        for (SpecParam specParam : specParams) {
            //先得到唯一的参数名
            String fieldName = specParam.getName();
            //循环添加要聚合的字段   terms是可以支持keywork的，不分词，这里terms（name）是聚合的名称，然后要聚合的字段
            nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(fieldName).field("specs." + fieldName + ".keyword"));
        }
        //3.获取结果
        AggregatedPage<Goods> paramResult = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), Goods.class);
        //4. 解析结果
        //4.1 得到所有的聚合
        Aggregations aggregations = paramResult.getAggregations();
        //4.2取出对应的聚合信息
        for (SpecParam specParam : specParams) {
            //这个参数名字同时也是我们定好的聚合名称
            String aggName = specParam.getName();
            //得到，并且同意转换成字符串形式（因为List<Map<String, String>>）
            StringTerms trems = aggregations.get(aggName);
            //得到Buckets中所有的聚合类型，根据里面的key，吧key转换 成这个结合（这个key就是 参数的字段可选项）
            List<String> options = trems.getBuckets().stream().map(b -> b.getKeyAsString()).collect(Collectors.toList());

            //准备map
            Map<String, Object> map = new HashMap<>();
            map.put("k", aggName); //这个就是规格参数的名称
            map.put("options", options); //这个就是规格参数的名称对应的vlue 参数可选项
            specs.add(map);
        }
        return specs;
    }


    /**
     * 解析品牌的聚合结果
     * <p>
     * Get /goods/_search
     * {
     * "size": 0,
     * "aggs": {
     * "brandAgg":{
     * "terms": {
     * "field": "brandId",
     * "size": 10
     * }
     * }
     * }
     * }
     * <p>
     * {
     * "took": 488,
     * "timed_out": false,
     * "_shards": {
     * "total": 1,
     * "successful": 1,
     * "skipped": 0,
     * "failed": 0
     * },
     * "hits": {
     * "total": 184,
     * "max_score": 0,
     * "hits": []
     * },
     * "aggregations": {
     * "brandAgg": {
     * "doc_count_error_upper_bound": 0,
     * "sum_other_doc_count": 4,
     * "buckets": [
     * {
     * "key": 8557,
     * "doc_count": 80
     * },
     * {
     * "key": 18374,
     * "doc_count": 47
     * },
     * {
     * "key": 12669,
     * "doc_count": 18
     * },
     * {
     * "key": 25591,
     * "doc_count": 11
     * },
     * {
     * "key": 15127,
     * "doc_count": 9
     * },
     * {
     * "key": 27306,
     * "doc_count": 4
     * },
     * {
     * "key": 91515,
     * "doc_count": 4
     * },
     * {
     * "key": 2032,
     * "doc_count": 3
     * },
     * {
     * "key": 27094,
     * "doc_count": 2
     * },
     * {
     * "key": 38126,
     * "doc_count": 2
     * }
     * ]
     * }
     * }
     * }
     *
     * @param terms 这里用的是Aggregation的实现类来接受，应为是按照品牌id进行聚合的，省的等下还要转，所以为LongTerms
     * @return
     */
    private List<Brand> parseBrandAgg(LongTerms terms) {
        //这个并不是想要的,想要的是Brand
//        List<LongTerms.Bucket> buckets = terms.getBuckets();


        //terms.getBuckets():拿到一堆的buckets
        //得到流，然后通过这个bucket拿到里面的KeyAsNumber，相当于品牌id吧，然后去的long类型值
        //然后这样给他转换成集合
        try {
            List<Long> ids = terms.getBuckets()
                    .stream()
                    .map(b -> b.getKeyAsNumber().longValue())
                    .collect(Collectors.toList());
            //根据ids查询品牌集合
            List<Brand> brandList = brandClient.queryBrandByIds(ids);
            return brandList;
        } catch (Exception e) {
            log.error("[搜索服务]解析品牌聚合结果时查询品牌集合异常", e);
            return null;
        }
    }


    /**
     * 解析分类的聚合结果
     *
     * @param terms 这里用的是Aggregation的实现类来接受，应为是按照商品类型id进行聚合的，省的等下还要转，所以为LongTerms
     * @return
     */
    private List<Category> parseCategoryAgg(LongTerms terms) {
        try {
            List<Long> ids = terms.getBuckets()
                    .stream()
                    .map(c -> c.getKeyAsNumber().longValue())
                    .collect(Collectors.toList());
            List<Category> categoryList = categoryClient.queryByIds(ids);
            return categoryList;
        } catch (Exception e) {
            log.error("[搜索服务]解析商品类型聚合结果时查询商品类型集合异常", e);
            return null;
        }
    }

    /**
     * 新增或修改索引库
     * @param spuId
     */
    public void createOrUpdateIndex(Long spuId) {
        //查询spu
        Spu spu = goodsClient.querySpuById(spuId);
        //构建goods
        Goods goods = buildGoods(spu);
        //存入索引库
        goodsRepository.save(goods);
    }

    /**
     * 对索引库进行删除
     * @param spuId
     */
    public void indexDelete(Long spuId) {
        goodsRepository.deleteById(spuId);
    }
}
